var _w = window,
    _b = document.body,
    _d = document.documentElement;

function getPoint(e: MouseEvent) {
    var scrollX = Math.max(0, _w.pageXOffset || _d.scrollLeft || _b.scrollLeft || 0) - (_d.clientLeft || 0),
        scrollY = Math.max(0, _w.pageYOffset || _d.scrollTop || _b.scrollTop || 0) - (_d.clientTop || 0),
        pointX = e ? (Math.max(0, e.pageX || e.clientX || 0) - scrollX) : 0,
        pointY = e ? (Math.max(0, e.pageY || e.clientY || 0) - scrollY) : 0;

    return { x: pointX, y: pointY };
}

interface SortableOptions {
    connector?: HTMLElement[],
    // appendChild?: () => HTMLElement
    onRelease?: (hoverItem: HTMLElement, clickItem: HTMLElement, hoverItemIndex?: number) => void;
}

let errors = {
    arugmentNull(name: string) {
        return new Error(`Argument ${name} is null or empty.`);
    }
}

export class Sortable {
    protected _container: HTMLElement;
    private _options: SortableOptions;
    private _clickItem: HTMLElement;
    protected _dragItem: HTMLElement;
    private _hovItem: HTMLElement;
    protected _sortLists: HTMLElement[] | NodeListOf<HTMLElement>;
    protected _click: { x?: number, y?: number };
    protected _dragging: boolean;

    private hoverInsertClassName = "hover-insert";
    private hoverAppendClassName = "hover-append";

    constructor(container: HTMLElement, options?: SortableOptions) {
        if (!container)
            throw errors.arugmentNull("container");

        this._container = container as HTMLElement;
        this._options = options || {}; /* nothing atm */
        this._clickItem = null;
        this._dragItem = null;
        this._hovItem = null;
        this._sortLists = [];
        this._click = {};
        this._dragging = false;

        this._container.style.position = "static";

        window.addEventListener("mousedown", this.onPress.bind(this), true);
        window.addEventListener("touchstart", this.onPress.bind(this), true);
        window.addEventListener("mouseup", (e) => this.onRelease(e), true);
        window.addEventListener("touchend", (e) => this.onRelease(e), true);
        window.addEventListener("mousemove", this.onMove.bind(this), true);
        window.addEventListener("touchmove", this.onMove.bind(this), true);
    }

    toArray(attr?) {
        attr = attr || "id";

        var data = [],
            item = null,
            uniq = "";

        for (var i = 0; i < this._container.children.length; ++i) {
            item = this._container.children[i],
                uniq = item.getAttribute(attr) || "";
            uniq = uniq.replace(/[^0-9]+/gi, "");
            data.push(uniq);
        }
        return data;
    }

    // serialize order array into a string 
    toString(attr?: any, delimiter?: any) {
        delimiter = delimiter || ":";
        return this.toArray(attr).join(delimiter);
    }

    // checks if mouse x/y is on top of an item 
    protected _isOnTop(item: Element, x: number, y: number) {
        var box = item.getBoundingClientRect(),
            isx = (x > box.left && x < (box.left + box.width)),
            isy = (y > box.top && y < (box.top + box.height));
        return (isx && isy);
    }

    private _isBelow(item: Element, x: number, y: number) {
        var box = item.getBoundingClientRect();
        var isx = (x > box.left && x < (box.left + box.width));
        var isy = y > (box.top + box.height);
        return isx && isy;
    }

    // manipulate the className of an item (for browsers that lack classList support)
    private _itemClass(item: Element, task: "add" | "remove", className: string) {
        var list = item.className.split(/\s+/),
            index = list.indexOf(className);

        if (task === "add" && index == -1) {
            list.push(className);
            item.className = list.join(" ");
        }
        else if (task === "remove" && index != -1) {
            list.splice(index, 1);
            item.className = list.join(" ");
        }
    }


    // update item position 
    protected _moveItem(item: any, x: number, y: number) {
        item.style["-webkit-transform"] = "translateX( " + x + "px ) translateY( " + y + "px )";
        item.style["-moz-transform"] = "translateX( " + x + "px ) translateY( " + y + "px )";
        item.style["-ms-transform"] = "translateX( " + x + "px ) translateY( " + y + "px )";
        item.style["transform"] = "translateX( " + x + "px ) translateY( " + y + "px )";
    }

    // make a temp fake item for dragging and add to container 
    private _makeDragItem(item: HTMLElement) {
        this._trashDragItem();
        // this._sortLists = document.querySelectorAll("[data-is-sortable]");
        this._sortLists = this._options.connector || []; //(this._options.connect || []).map(o => o._container);
        this._sortLists.push(this._container);

        this._clickItem = item;
        this._itemClass(this._clickItem, "add", "active");

        this._dragItem = document.createElement(item.tagName);
        this._dragItem.className = "dragging";
        this._dragItem.innerHTML = item.innerHTML;
        this._dragItem.style.position = "absolute";
        this._dragItem.style.zIndex = "9999";
        this._dragItem.style.left = (item.offsetLeft || 0) + "px";
        this._dragItem.style.top = (item.offsetTop || 0) + "px";
        this._dragItem.style.width = (item.offsetWidth || 0) + "px";

        this._container.appendChild(this._dragItem);
    }

    // remove drag item that was added to container 
    private _trashDragItem() {
        if (this._dragItem && this._clickItem) {
            this._itemClass(this._clickItem, "remove", "active");
            this._clickItem = null;

            this._container.removeChild(this._dragItem);
            this._dragItem = null;
        }
    }

    // on item press/drag 
    private onPress(e: MouseEvent) {
        if (e.target == null)
            return;

        let itemElement: HTMLElement = e.target as HTMLElement;
        while (itemElement.parentNode != this._container && itemElement.parentNode != null) {
            itemElement = itemElement.parentNode as HTMLElement;
        }

        if (itemElement.parentNode === this._container) {
            e.preventDefault();

            this._dragging = true;
            this._click = getPoint(e);
            this._makeDragItem(itemElement);
            this.onMove(e);
        }
    }

    // on item release/drop 
    private onRelease(e: MouseEvent | TouchEvent) {
        this._dragging = false;

        let p = this._clickItem ? this._clickItem.parentNode : null;
        let isCurrent = false;
        while (p) {
            if (this._container == p) {
                isCurrent = true;
                break;
            }
            p = p.parentNode;
        }

        if (isCurrent && this._options.onRelease) {
            let itemIndex: number;
            if (this._hovItem != null && this._hovItem.className.indexOf(this.hoverInsertClassName) >= 0) {
                for (let i = 0; i < this._hovItem.parentNode.children.length; i++) {
                    if (this._hovItem.parentNode.children[i] == this._hovItem) {
                        itemIndex = i;
                        break;
                    }
                }
            }

            this._options.onRelease.bind(this)(this._hovItem, this._clickItem, itemIndex);
        }

        if (this._hovItem) {
            this._hovItem.className = "";
        }

        this._trashDragItem();
    }

    // on item drag/move
    protected onMove(e: MouseEvent) {
        if (!this._dragItem || !this._dragging)
            return;

        e.preventDefault();

        var point = getPoint(e);
        var container: HTMLElement;
        // drag fake item 
        this._moveItem(this._dragItem, (point.x - this._click.x), (point.y - this._click.y));

        // keep an eye for other sortable lists and switch over to it on hover 
        for (var a = 0; a < this._sortLists.length; ++a) {
            var subContainer = this._sortLists[a];

            if (this._isOnTop(subContainer, point.x, point.y)) {
                container = subContainer;
            }
        }

        if (container == null) {
            return;
        }

        /*
        // container is empty, move clicked item over to it on hover 
        if (this._isOnTop(container, point.x, point.y) && container.children.length === 0) {
            container.appendChild(this._clickItem);
            return;
        }
        */

        let latestItem: Element = null;
        if (container == this._container) {
            if (container.children.length > 1) {
                // 最后一个子元素是 dragItem
                latestItem = container.children[container.children.length - 2];
            }
        }
        else {
            latestItem = container.children[container.children.length - 1];
        }

        if (this._hovItem) {
            this._hovItem.className = "";
        }

        for (var b = 0; b < container.children.length; ++b) {
            var subItem = container.children[b];

            if (subItem === this._clickItem || subItem === this._dragItem) {
                continue;
            }

            if (this._isOnTop(subItem, point.x, point.y)) {
                this._hovItem = subItem as HTMLElement;
                this._itemClass(this._hovItem, "add", this.hoverInsertClassName);
            }
            else if (this._hovItem != null && latestItem != null && this._isBelow(latestItem, point.x, point.y)) {
                this._itemClass(this._hovItem, "add", this.hoverAppendClassName);
            }
        }
    }
}

export class Dragable extends Sortable {
    protected onMove(e: MouseEvent) {
        if (!this._dragItem || !this._dragging)
            return;

        e.preventDefault();

        var point = getPoint(e);
        var container: HTMLElement;
        // drag fake item 
        this._moveItem(this._dragItem, (point.x - this._click.x), (point.y - this._click.y));

        // keep an eye for other sortable lists and switch over to it on hover 
        for (var a = 0; a < this._sortLists.length; ++a) {
            var subContainer = this._sortLists[a];

            if (this._isOnTop(subContainer, point.x, point.y)) {
                container = subContainer;
            }
        }

        if (container == this._container) {
            return;
        }

        super.onMove(e);

    }
}
